using System.Collections.Generic;
using System;
using System.Web.Script.Serialization;
using System.Text;
using System.Web;

namespace Gigya.Socialize.SDK 
{
    /// <summary>  
    /// Used for passing parameters when issueing requests e.g. GSRequest.send
    /// As well as returning response data e.g. GSResponse.getData
    /// The dictionary can hold the following types: string, boolean, int, long, Array of GSObjects, GSObject    
    /// </summary>
    /// <remarks>Author: Tamir Korem. Updated by: Yaron Thurm</remarks>
    [Serializable]
    public class GSDictionary 
    {
        // Using SortedDictionary to ensure alphabetic order of keys.
        // Important when calculating base string for OAuth1 signatures
        private SortedDictionary<string, object> map = new SortedDictionary<string, object>(StringComparer.Ordinal);

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public GSDictionary() {}
    		    
        /// <summary>
        /// Construct a GSDictionary from json string.
        /// Throws exception if unable to parse json
        /// </summary>
        /// <param name="json">the json formatted string</param>        
	    public GSDictionary(string json)
	    {
            if (!String.IsNullOrEmpty(json))
            {
                GSDictionary.processJsonObject(json, this);
            }
	    }

        /// <summary>
        /// Construct a GSDictionary from a JSONObject - used internally.
        /// throws exception if unable to parse json
        /// </summary>
        /// <param name="jsonObj">the json object to parse</param>
        private GSDictionary(Dictionary<string, object> jsonObj) // throw Exception
	    {
            GSDictionary.processJsonObject(jsonObj, this);
        }
        #endregion


        #region - PUTS -
        /// <summary>
        ///  Associates the specified value with the specified key in this dictionary. 
        ///  If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">a string value to be associated with the specified key</param>
        public void put(string key, string value)
	    {
		    if (key == null) return;
            this.map[key] = value;
	    }
    
        /// <summary>
        ///  Associates the specified value with the specified key in this dictionary. 
        ///  If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">an int value to be associated with the specified key </param>
	    public void put(string key, int value)
	    {
		    if (key == null) return;
            this.map[key] = value;
	    }
    	
        /// <summary>
        /// Associates the specified value with the specified key in this dictionary.
        /// If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">a long value to be associated with the specified key </param>
        public void put(string key, long value)
        {
            if (key == null) return;
            this.map[key] = value;
        }

        /// <summary>
        /// Associates the specified value with the specified key in this dictionary.
        /// If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">a bool value to be associated with the specified key</param>
	    public void put(string key, bool value)
	    {
		    if (key == null) return;
            this.map[key] = value;
	    }
    	       
        /// <summary>
        /// Associates the specified value with the specified key in this dictionary. 
        /// If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">a GSDictionary value to be associated with the specified key </param>
	    public void put(string key, GSDictionary value)
	    {
		    if (key == null) return;
            this.map[key] = value;
	    }
    	
        /// <summary>
        /// Associates the specified value with the specified key in this dictionary. 
        /// If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">a GSDictionary[] value to be associated with the specified key</param>
	    public void put(string key, GSDictionary[] value)
        {
            if (key == null) return;
            this.map[key] = value;
        }
        
        #endregion
        

        #region - GETS -
        /* GET BOOL */
        /// <summary>
        /// Returns the bool value to which the specified key is mapped, or the 
        /// defaultValue if this dictionary contains no mapping for the key.
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <param name="defaultValue">the bool value to be returned if this dictionary doesn't contain the specified key.</param>
        /// <returns>the bool value to which the specified key is mapped, or the defaultValue if 
        /// this dictionary contains no mapping for the key.</returns>
        public bool getBool(string key, bool defaultValue)
        {
            bool retVal = defaultValue;
            try { retVal = (bool)this.getTypedObject(key, "bool", defaultValue, true);}
            catch { }

            return retVal;
        }        
        /// <summary>
        /// Returns the bool value to which the specified key is mapped. 
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <returns>the bool value to which the specified key is mapped.</returns>
        /// <exception cref="Gigya.Socialize.SDK.GSKeyNotFoundException">thrown if the key is not found</exception>
        /// <exception cref="System.FormatException">thrown if the value cannot be parsed as bool</exception>
        public bool getBool(string key)
        {            
            bool retVal = (bool)this.getTypedObject(key, "bool", null, false);            
            return retVal;
        }


        /* GET INTEGER */
        /// <summary>
        /// Returns the int value to which the specified key is mapped, or the 
        /// defaultValue if this dictionary contains no mapping for the key.
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <param name="defaultValue">the int value to be returned if this dictionary doesn't contain the specified key.</param>
        /// <returns>the int value to which the specified key is mapped, or the defaultValue if 
        /// this dictionary contains no mapping for the key.</returns>
        public int getInt(string key, int defaultValue)
        {
            int retVal = defaultValue;
            try { retVal = (int)this.getTypedObject(key, "int", defaultValue, true); }
            catch { }

            return retVal;
        }
        /// <summary>
        /// Returns the int value to which the specified key is mapped. 
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <returns>the int value to which the specified key is mapped.</returns>
        /// <exception cref="Gigya.Socialize.SDK.GSKeyNotFoundException">thrown if the key is not found</exception>
        /// <exception cref="System.FormatException">thrown if the value cannot be parsed as int</exception>
        public int getInt(string key)
        {
            int retVal = (int)this.getTypedObject(key, "int", null, false);
            return retVal;
        }


        /* GET LONG */
        /// <summary>
        /// Returns the long value to which the specified key is mapped, or the defaultValue
        /// if this dictionary contains no mapping for the key.
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <param name="defaultValue">the long value to be returned if this dictionary doesn't contain the specified key.</param>
        /// <returns>the long value to which the specified key is mapped, or the defaultValue if this 
        /// dictionary contains no mapping for the key</returns>       
        public long getLong(string key, long defaultValue)
        {
            long retVal = defaultValue;
            try { retVal = (long)this.getTypedObject(key, "long", defaultValue, true); }
            catch { }

            return retVal;
        }
        /// <summary>
        /// Returns the long value to which the specified key is mapped. 
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <returns>the long value to which the specified key is mapped.</returns>
        /// <exception cref="Gigya.Socialize.SDK.GSKeyNotFoundException">thrown if the key is not found</exception>
        /// <exception cref="System.FormatException">thrown if the value cannot be parsed as long</exception>
        public long getLong(string key)
        {
            long retVal = (long)this.getTypedObject(key, "long", null, false);
            return retVal;
        }


        /* GET STRING */
        /// <summary>
        /// Returns the string value to which the specified key is mapped, or the defaultValue
        /// if this dictionary contains no mapping for the key.
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <param name="defaultValue">the string value to be returned if this dictionary doesn't contain the specified key.</param>
        /// <returns>the string value to which the specified key is mapped, or the defaultValue if this 
        /// dictionary contains no mapping for the key</returns>
        public string getString(string key, string defaultValue)
        {
            string retVal = defaultValue;
            try { retVal = (string)this.getTypedObject(key, "string", defaultValue, true); }
            catch { }

            return retVal;           
        }
        /// <summary>
        /// Returns the string value to which the specified key is mapped. 
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <returns>the string value to which the specified key is mapped.</returns>
        /// <exception cref="Gigya.Socialize.SDK.GSKeyNotFoundException">thrown if the key is not found</exception>
        public string getString(string key)
        {
            string retVal = (string)this.getTypedObject(key, "string", null, false);
            return retVal;           
        }


        /* GET GSOBJECT */
        /// <summary>
        /// Returns the GSDictionary value to which the specified key is mapped, or the defaultValue
        /// if this dictionary contains no mapping for the key.
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <param name="defaultValue">the GSDictionary value to be returned if this dictionary doesn't contain the specified key.</param>
        /// <returns>the GSDictionary value to which the specified key is mapped, or the defaultValue if this 
        /// dictionary contains no mapping for the key</returns>
        public GSDictionary getObject(string key, GSDictionary defaultValue)
        {
            GSDictionary retVal = defaultValue;
            try { retVal = (GSDictionary)this.getTypedObject(key, "GSDictionary", defaultValue, true); }
            catch { }

            return retVal;
        }
        /// <summary>
        /// Returns the GSDictionary value to which the specified key is mapped. 
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <returns>the GSDictionary value to which the specified key is mapped.</returns>
        /// <exception cref="Gigya.Socialize.SDK.GSKeyNotFoundException">thrown if the key is not found</exception>
        /// <exception cref="System.InvalidCastException">thrown if the value cannot be cast to GSDictionary</exception>
        public GSDictionary getObject(string key)
        {
            GSDictionary retVal = (GSDictionary)this.getTypedObject(key, "GSDictionary", null, false);
            return retVal;
        }
        
        
        /* GET GSOBJECT[] */
        /// <summary>
        /// Returns the GSDictionary[] value to which the specified key is mapped, or the defaultValue
        /// if this dictionary contains no mapping for the key.
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <param name="defaultValue">the GSDictionary[] value to be returned if this dictionary doesn't contain the specified key.</param>
        /// <returns>the GSDictionary[] value to which the specified key is mapped, or the defaultValue if this 
        /// dictionary contains no mapping for the key</returns>
        public GSDictionary[] getArray(string key, GSDictionary[] defaultValue)
        {
            GSDictionary[] retVal = defaultValue;
            try { retVal = (GSDictionary[])this.getTypedObject(key, "GSDictionary[]", defaultValue, true); }
            catch { }

            return retVal;
        }
        /// <summary>
        /// Returns the GSDictionary[] value to which the specified key is mapped. 
        /// </summary>
        /// <param name="key">the key whose associated value is to be returned</param>
        /// <returns>the GSDictionary[] value to which the specified key is mapped.</returns>
        /// <exception cref="Gigya.Socialize.SDK.GSKeyNotFoundException">thrown if the key is not found</exception>
        /// <exception cref="System.InvalidCastException">thrown if the value cannot be cast to GSDictionary[]</exception>
        public GSDictionary[] getArray(string key)
        {
            GSDictionary[] retVal = (GSDictionary[])this.getTypedObject(key, "GSDictionary[]", null, false);
            return retVal;
        }

        #endregion


        #region Other public methods
        /// <summary>
        /// Returns true if this dictionary contains a mapping for the specified key.
        /// </summary>
        /// <param name="key">key whose presence in this map is to be tested</param>
        /// <returns>true if this map contains a mapping for the specified key</returns>
        public bool containsKey(string key)
		{
			return this.map.ContainsKey(key);
		}
    	
        /// <summary>
        /// Parse parameters from URL into the dictionary
        /// </summary>
        /// <param name="url">the URL string to parse</param>
	    public void parseURL(string url)
	    {
		    try 
            {
                Uri u = new Uri(url);

                // Parse the query string part of the uri
                this.parseQuerystring(u.Query);

                // Parse the fragment part of the uri
                this.parseQuerystring(u.Fragment);
	        } 
            catch (UriFormatException) { } 
	    }

        /// <summary>
        /// Parse parameters from query string
        /// </summary>
        /// <param name="qs">The query string to parse</param>
	    public void parseQuerystring(string qs)
	    {
		    if (qs == null) return;
            
            if (qs.StartsWith("?")) qs = qs.Remove(0, "?".Length); // Remove QuestionMark before query string
            if (qs.StartsWith("#")) qs = qs.Remove(0, "#".Length); // Remove Pound sign before fragment part
                            
            string[] array = qs.Split('&');
            foreach (string parameter in array) 
            {
				int indexOf = parameter.IndexOf("=");
				if (indexOf == -1) continue;
				string key = parameter.Substring(0, indexOf);
				string value = parameter.Substring(indexOf + 1);
        	    try 
                {
					this.put(key, HttpUtility.UrlDecode(value, Encoding.UTF8));
        	    } 
                catch (Exception){}
            }
	    }

        /// <summary>
        /// Removes the key (and its corresponding value) from this dictionary. 
        /// This method does nothing if the key is not in this dictionary.  
        /// </summary>
        /// <param name="key">the key that needs to be removed.</param>
	    public void remove(string key)
	    {
		   this.map.Remove(key);
	    }

        /// <summary>
        /// Removes all of the entries from this dictionary. The dictionary will be empty after this call returns. 
        /// </summary>
	    public void clear()
	    {
            this.map.Clear();
	    }

        /// <summary>
        /// Returns a String array containing the keys in this dictionary. 
        /// </summary>
        /// <returns>a KeyCollection of the keys in this dictionary.</returns>
        public SortedDictionary<string, object>.KeyCollection getKeys()
	    {
            return this.map.Keys;
	    }

        /// <summary>
        /// Returns the dictionary's content as a JSON string. 
        /// </summary>
        /// <returns>the dictionary's content as a JSON string.</returns>
	    public override string ToString()
	    {            
		    return this.toJsonString();
	    }

        /// <summary>
        /// Returns the dictionary's content as a JSON string. 
        /// </summary>
        /// <returns>the dictionary's content as a JSON string.</returns>
	    public string toJsonString()
	    {
		    try 
            {
                SortedDictionary<string, object> obj = this.toJsonObject();
                return new JavaScriptSerializer().Serialize(obj);
		    } 
            catch (Exception)
		    {
			    return null;
		    }
        }

        /// <summary>
        /// Returns a deep clone of the current instance
        /// </summary>
        /// <returns></returns>
        public GSDictionary clone()
        {
            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formater = null;
            System.IO.MemoryStream stream = null;
            GSDictionary ret = null;
            try
            {
                // Serialize object
                formater = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
                stream = new System.IO.MemoryStream();
                formater.Serialize(stream, this);

                // Deserialize it
                stream.Position = 0;
                ret = (GSDictionary)formater.Deserialize(stream);
            }
            catch (Exception ex)
            {
                if (stream != null)
                    stream.Close();
            }

            return ret;
        }
        #endregion


        #region Private Methods

        /// <summary>
        /// Associates the specified value with the specified key in this dictionary. 
        /// If the dictionary previously contained a mapping for the key, the old value is replaced by the specified value.
        /// Only for private use by this class
        /// </summary>
        /// <param name="key">key with which the specified value is to be associated</param>
        /// <param name="value">an object value to be associated with the specified key</param>      
        private void put(string key, object value)
        {
            if (key == null) return;
            this.map[key] = value;
        }

        /// <summary>
        /// Returns the value for a given key. 
        /// If the key is not found then:
        /// If the useDefaultValue is true, then the defaultValue is return, otherwise a key not found exception is thrown.
        /// If the key is found then:
        /// If the object value can be parsed according to the type requested, it is returned after parsing.
        /// If parsing fails, then a FormatException exception is thrown
        /// </summary>
        /// <param name="key">the key to search for</param>
        /// <param name="type">string representing the type of the requested value. Can be one of the following:
        /// "bool", "int", "long", "string", "GSDictionary", "GSDictionary[]" (not case sensitive)</param>
        /// <param name="defaultValue">value to return if key is not found</param>
        /// <param name="useDefaultValue">whether or not to use the defaultValue if key is not found</param>
        /// <returns></returns>        
        private object getTypedObject(string key, string type, object defaultValue, bool useDefaultValue)
        {
            object val;
            // Search for the key
            if (this.map.TryGetValue(key, out val) /* key was found and has be places into obj*/)
            {
                if (val == null)
                    return null;

                if (type.ToLower() == "bool")
                    return bool.Parse(val.ToString());
                else if (type.ToLower() == "int")
                    return int.Parse(val.ToString());
                else if (type.ToLower() == "long")
                    return long.Parse(val.ToString());
                else if (type.ToLower() == "string")
                    return val.ToString();
                else if (type.ToLower() == "gsdictionary")
                    return (GSDictionary)val;
                else if (type.ToLower() == "gsdictionary[]")
                    return (GSDictionary[])val;
                else
                    throw new System.Exception("Unsupported parameter. 'type' can only be one of the following:\n" +
                        "bool, int, long, string, GSDictionary, GSDictionary[]");
            }
            else /* key couldn't be found */
            {
                if (useDefaultValue)
                    return defaultValue;
                else
                    throw new GSKeyNotFoundException("GSDicitonary does not contain a value for key " + key);
            }
        }

        private SortedDictionary<string, object> toJsonObject()
	    {
            SortedDictionary<string, object> jsonObj = new SortedDictionary<string, object>();
            SortedDictionary<string, object>.KeyCollection keys = this.getKeys();
		    object val;
		    foreach (string key in keys)
		    {
                val = this.map[key];
			    if (val is GSDictionary)
			    {
                    jsonObj[key] = ((GSDictionary)val).toJsonObject();
			    } 
                else if (val is GSDictionary[])
			    {
				    GSDictionary[] gsdArray = this.getArray(key);
                    SortedDictionary<string, object>[] jsonArray = new SortedDictionary<string, object>[gsdArray.Length];                    
                    int index = 0;
				    foreach (GSDictionary gsd in gsdArray)
                    {
                        jsonArray[index++] = gsd.toJsonObject();
                    }
                    jsonObj[key]= jsonArray;
			    } 
                else
			    {
                    jsonObj[key]= val;
			    }
		    }
            return jsonObj;
	    }

        private static Dictionary<string, object> getJsonObj(string jsonStr)
        {
            return (Dictionary<string,object>)new JavaScriptSerializer().DeserializeObject(jsonStr);
        }

        private static void processJsonObject(string jsonStr, GSDictionary parentObj)
        {
            Dictionary<string, object> jsonObj = getJsonObj(jsonStr);
            GSDictionary.processJsonObject(jsonObj, parentObj);
        }

        private static void processJsonObject(Dictionary<string, object> jsonObj, GSDictionary parentObj)
        {
            string key;
            object value;
            foreach (KeyValuePair<string, object> kvp in jsonObj)
            {
                key = kvp.Key;
                value = kvp.Value;
                
                if (value == null)
                    parentObj.put(key, value); // null values are allowed
                else if (value.GetType().IsPrimitive || value.GetType() == typeof(string))
                    parentObj.put(key, value);
                else if (value is Dictionary<string, object>) // value is a dictionary (a sub item in json)
                {
                    // Create a new GSDictionary to put as the value of the current key. the source for this child is the current value
                    GSDictionary child = new GSDictionary((Dictionary<string, object>)value);
                    // Put the child as the value of the current key
                    parentObj.put(key, child);
                }
                else if (value is object[]) // value is an array
                {
                    object[] values = (object[])value;
                    List<GSDictionary> childArray = new List<GSDictionary>();
                    //GSDictionary[] childArray = new GSDictionary[count];
                    for (int i = 0; i < values.Length; i++)
                    {
                        // Check to see if the object is a Dictionary object
                        if (values[i] is Dictionary<string, object>)
                        {
                            GSDictionary newChild = new GSDictionary((Dictionary<string, object>)values[i]);
                            childArray.Add(newChild);
                        }
                    }

                    // Put the childArray as a value for the current key
                    parentObj.put(key, childArray.ToArray());
                }
            }
        }
        
        public object getRawObject(String key)
        {
            return map[key];
        }

        #endregion
    }
}
